Skip to content

refactor: migrate shell layer to tinyexec + per-command e2e tests#210

Merged
rqbazan merged 5 commits into
mainfrom
refactor/shell-quoting-and-vland-init
May 12, 2026
Merged

refactor: migrate shell layer to tinyexec + per-command e2e tests#210
rqbazan merged 5 commits into
mainfrom
refactor/shell-quoting-and-vland-init

Conversation

@rqbazan
Copy link
Copy Markdown
Member

@rqbazan rqbazan commented May 12, 2026

Summary

Three intertwined improvements landing together because they share the same shell layer rewrite:

  1. @vlandoss/clibuddy (minor, breaking pre-1.0): replace zx with tinyexec. New ShellService API:

    • run(cmd, args, opts?) — verbose, streams stdio, prints $ <cmd> <args> prefix.
    • runCaptured(cmd, args, opts?) — silent, returns Output { stdout, stderr, exitCode }.
    • Both throw NonZeroExitError on non-zero exit by default; opt out with { throwOnError: false }.
    • at(cwd) / child(opts) for inherited config.
    • New resolvePackageBin(pkg, { from, binName? }) async helper that finds an installed package's bin even when exports is restrictive (oxlint) or absent (@biomejs/biome). Built on pkg-types (already a dep) + a one-line fallback for packages without main/exports. Memoised per (pkg, from, binName) via memoize (new dep, version aligned with run-run).
    • isNonZeroExitError replaces isProcessOutput.
    • Removed: $\...`template-literal API,quote/isRaw/defaultQuote, getPreferLocal/localBaseBinPath, mute()/quiet(), ./test-helpers` export.
    • tinyexec auto-prepends every parent node_modules/.bin to PATH, so localBaseBinPath is no longer needed.
  2. @vlandoss/run-run (patch):

    • ToolService now carries bin resolution via constructor metadata: subclasses declare { pkg, bin?, ui } and the base provides a concrete getBinDir() that calls resolvePackageBin. Each subclass (biome, oxlint, oxfmt, tsdown, tsc) is just a super(...) plus its tool-specific operations — no per-subclass override getBinDir. The absolute path bypasses the node_modules/.bin/<bin> shims that run-run itself publishes (tools/biome etc.), which would otherwise loop back through rr tools <bin> indefinitely.
    • ToolService.exec accepts only string[]. tscheck runs pretsc/pretypecheck with shell: true so they can use &&, pipes, env-var substitution.
    • Bump tsdown 0.21.10 → 0.22.0. 0.21.x pulled in a broken unrun@0.2.38 tarball (missing dist/) producing WARN Failed to create bin … unrun on every install; 0.22 dropped the dep entirely.
    • End-user CLI behaviour unchanged.
  3. @vlandoss/vland (minor): vland init prompts (default-yes) for installing dependencies and initialising git when --install/--no-install/--git/--no-git aren't passed. Non-interactive defaults: both true. Also: the git-commit-failure (pathspec 'initial' did not match) caused by the old custom quote() not wrapping whitespace strings is gone — array-based exec makes each token survive. The pnpm install spinner is now silent (was leaking deprecation + ENOENT warnings). Project-name prompt placeholder and outro pnpm <script> line are template-aware (my-lib + pnpm test for library; my-api + pnpm dev for backend; my-mono + pnpm dev for monorepo).

Tests reorganised

  • Dropped clibuddy/test-helpers (the zx mock). e2e is the strategy now.
  • run-run integration suite split into one file per command: cli, jsc, lint, format, tsc, build-lib. Each spawns the real rr binary against a temp fixture (makeFixture helper) and asserts on observable output.
  • vland init split similarly: init-templates, init-git, init-install.

Test plan

  • pnpm test — 23 tests across all integration suites
  • pnpm test:types — 4/4 packages clean
  • Manual smoke from packages/vland: rr jsc, rr lint, rr format, rr tsc all exit 0 with each flag separated in the verbose trace
  • Manual smoke: vland init my-app -t library --git --no-install produces a single chore: initial commit from vland; vland init --git (with install) shows clean spinner with no pnpm leak
  • Confirmed unrun ENOENT and DEP0169 warnings are gone after tsdown bump
  • CI green
  • Bot opens the Version PR with the expected bumps:
    • @vlandoss/clibuddy 0.5.0 → 0.6.0 (minor, breaking pre-1.0)
    • @vlandoss/vland 0.1.1 → 0.2.0 (minor)
    • @vlandoss/run-run 0.5.2 → 0.5.3 (patch)

🤖 Generated with Claude Code

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 12, 2026

🦋 Changeset detected

Latest commit: 137024d

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@vlandoss/clibuddy Minor
@vlandoss/run-run Patch
@vlandoss/vland Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Big single landing for three intertwined improvements that were
prepped on this branch:

1. **clibuddy: replace zx with tinyexec.** The new `ShellService` API
   is `run(cmd, args, opts?)` (verbose, streams stdio) and
   `runCaptured(cmd, args, opts?)` (silent, returns Output), both
   throwing `NonZeroExitError` by default. Drops `quote/isRaw/
   defaultQuote/getPreferLocal/localBaseBinPath/mute()/quiet()/
   isProcessOutput` and the `./test-helpers` export. Adds
   `resolveBinPath(pkg, { from, binPath?, binName? })` to find the
   absolute path to a package's bin even when its `exports` map is
   restrictive (oxlint) or absent (@biomejs/biome).

2. **run-run: use the new shell + tighten exec to string[].** Every
   `ToolService` now resolves its bin path with `resolveBinPath` and
   passes it to `ShellService.run` with `display: <friendly-name>`,
   bypassing the `node_modules/.bin/<bin>` shims that run-run itself
   publishes (tools/biome, tools/oxlint, tools/oxfmt, tools/tsdown)
   which would otherwise loop. `ToolService.exec` now accepts only
   `string[]`. `tscheck` runs pre-scripts with `shell: true` so they
   can use `&&`, pipes, env-var substitution.

3. **vland init: prompt for install/git + fix git commit.** Adds
   default-yes confirms when the user doesn't pass --install/--no-
   install / --git/--no-git on the CLI. The git-commit failure
   (`pathspec 'initial' did not match`) caused by missing $'…'
   wrapping in the old custom quote() goes away naturally with array-
   based exec.

**Tests reorganised.** Drop the clibuddy/test-helpers mock entirely.
run-run integration tests are split into one file per command
(cli, jsc, lint, format, tsc, build-lib), each spawning the real
`rr` binary against a temp fixture (`makeFixture` helper) and
asserting on observable output. vland init keeps its existing
integration coverage (commit message exact-match, no pathspec errors,
prompt-default behaviour in non-TTY).

Verification: pnpm test (22 tests across 7 files, all green) and
pnpm test:types (4 packages clean).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@rqbazan rqbazan force-pushed the refactor/shell-quoting-and-vland-init branch from d662e9c to 174d876 Compare May 12, 2026 04:35
`BiomeService.check()` calls `biome ci` instead of `biome check` when
`isCI` is true (std-env). The jsc tests asserted on `check` only,
which made them green locally but red on GitHub Actions. Relax the
regex to match either subcommand — the test's intent is that biome
receives properly tokenized flags, not which subcommand it's invoked
with.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@rqbazan rqbazan added the preview Ready to run CI/CD label May 12, 2026
@vland-bot
Copy link
Copy Markdown
Contributor

vland-bot Bot commented May 12, 2026

Preview release

Latest commit: 137024d

Some packages have been released:

Package Version Install
@vlandoss/clibuddy v0.5.1-git-137024d.0 @vlandoss/clibuddy@v0.5.1-git-137024d.0
@vlandoss/run-run v0.5.3-git-137024d.0 @vlandoss/run-run@v0.5.3-git-137024d.0
@vlandoss/vland v0.1.2-git-137024d.0 @vlandoss/vland@v0.1.2-git-137024d.0

Note

Use the PR number as tag to install any package. For instance:

pnpm add @vlandoss/clibuddy@pr-210

rqbazan and others added 3 commits May 11, 2026 23:46
The clack spinner already says "Installing dependencies with pnpm…"
but `installDependencies` from nypm was running with stdio inherited,
so deprecation warnings (DEP0169 url.parse), upstream `unrun` ENOENT
warnings, and pnpm's `Ignored build scripts` notice all leaked into
the terminal — confusing UX next to the spinner.

nypm exposes `silent: true` which pipes stdio. If install fails the
spinner already flips to "Failed to install dependencies" and we
debug-log the captured error.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The project-name prompt always suggested `my-app` regardless of which
template was picked, and the outro always recommended `pnpm dev` even
for the `library` template (which has no `dev` script in its
package.json).

Add a `TEMPLATE_META` map keyed by template name with:

- `placeholder` — used in the project-name `text()` prompt
- `runScript`   — appended to the outro's next-steps line

Mappings:
- library  → `my-lib`,  next: `pnpm test`  (libraries don't have a dev server)
- backend  → `my-api`,  next: `pnpm dev`
- monorepo → `my-mono`, next: `pnpm dev`   (turbo run dev)

Add a backend-specific install-resolution test alongside the existing
library one to lock in both branches.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Compress the bin-resolution helper from ~50 LOC of custom walk-up logic
to ~20 LOC leveraging `pkg-types` (already a clibuddy dep) plus
`memoize`. pkg-types' `resolvePackageJSON` handles oxlint/oxfmt-style
restrictive `exports` maps (the `.` entry is exposed → resolves to a
file inside the package → `findNearestFile` walks back up to package
.json). A one-line `createRequire(from).resolve(<pkg>/package.json)`
fallback covers `@biomejs/biome` (no `main`/`exports` at all).

API changes in clibuddy:
- Rename file `shell/resolve-bin.ts` → `shell/resolve-package-bin.ts`.
- Rename function `resolveBinPath` → `resolvePackageBin`.
- Drop the unused `binPath` option.
- Function is now async (pkg-types is async).
- Wrap with `memoize` keyed by `pkg|from|binName` so concurrent calls
  share the in-flight promise and `rr x jsc tsc` doesn't re-resolve.

run-run side:
- ToolService now provides a concrete `getBinDir()` that reads
  `{ pkg, bin?, ui }` from the constructor. Subclasses (biome, oxlint,
  oxfmt, tsdown, tsc) collapse to a single `super(...)` call plus their
  tool-specific operations — no more per-subclass `override getBinDir`.
- Bump tsdown 0.21.10 → 0.22.0 in run-run + tsdown-config peer dep.
  0.21.x pulled in `unrun@0.2.38` whose tarball is missing `dist/`,
  producing `WARN Failed to create bin … unrun` on every install. 0.22
  dropped unrun from dependencies.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@rqbazan rqbazan merged commit 7d7e673 into main May 12, 2026
3 checks passed
@rqbazan rqbazan deleted the refactor/shell-quoting-and-vland-init branch May 12, 2026 22:22
@vland-bot vland-bot Bot mentioned this pull request May 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

preview Ready to run CI/CD

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant